package weeny.pre.io;

import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import prefuse.data.Graph;
import prefuse.data.Table;
import prefuse.data.io.AbstractGraphReader;
import prefuse.data.io.DataIOException;
import prefuse.data.parser.CollectionParser;
import prefuse.data.parser.DataParseException;
import prefuse.data.parser.DataParser;
import prefuse.data.parser.ParserFactory;
import prefuse.util.collections.IntIterator;

import weeny.pre.display.Initialization;
import weeny.pre.io.MyGraphWriter.Tokens;
import weeny.pre.io.parser.ColorParser;
import weeny.util.logs.Debug;

/**
 * This reader supports several types of columns:
 * 
 * 1) Primitive ones: listed in {@link MyGraphWriter.Tokens}, such as int, long
 *    and String, their array types
 * 2) Anything can be found by {@link ParserFactory#getDefaultFactory()}, such
 *    as Font, Rectangle2D, etc. If you want to make something writable and
 *    readable, please write your own {@link DataParser} and add them to the
 *    default ParserFactory in {@link ParserFactory} or modify or inherit the
 *    {@link Initialization}. Anyway, make sure the parsers are there before you
 *    call {@link MyGraphReader#readGraph(InputStream)} function.
 * 3) Collection: the type should be concrete, which means neither abstract nor
 *    interface. And we also can make them nested, for example, ArrayLists in
 *    HashSets in LinkedList. Anyway, the final atom element should be in 1) or
 *    2), the higher elements should be always concrete collection type.
 * 
 * @author weeny
 *
 */
public class MyGraphReader extends AbstractGraphReader{
	
	public Graph safeRead(InputStream is){
		Graph graph = null;
		try {
			graph = readGraph(is);
		} catch (DataIOException e) {
			e.printStackTrace();
		}
		return graph;
	}
	public Graph safeRead(String file){
		try {
			Graph graph = readGraph(file);
			return graph;
		} catch (DataIOException e) {
			e.printStackTrace();
		}
		return null;
	}
	@Override
	public Graph readGraph(InputStream is) throws DataIOException {
		Initialization.init();
        try {       
            SAXParserFactory factory   = SAXParserFactory.newInstance();
            SAXParser        saxParser = factory.newSAXParser();
            
            GraphMLHandler   handler   = new GraphMLHandler();
            saxParser.parse(is, handler);
            return handler.getGraph();
        } catch ( Exception e ) {
            if ( e instanceof DataIOException ) {
                throw (DataIOException)e;
            } else {
                throw new DataIOException(e);
            }
        }
	}
    /**
     * A SAX Parser for GraphML data files.
     */
    public static class GraphMLHandler extends DefaultHandler implements Tokens
    {
        protected ParserFactory m_pf = ParserFactory.getDefaultFactory();
        
        protected static final String SRC = Graph.DEFAULT_SOURCE_KEY;
        protected static final String TRG = Graph.DEFAULT_TARGET_KEY;
        protected static final String SRCID = SRC+'_'+ID;
        protected static final String TRGID = TRG+'_'+ID;
        
        protected String m_graphid;
        protected Graph m_graph = null;
        protected Table m_nodes = new Table();
        protected Table m_edges = new Table();
        
        // schema parsing
        protected String m_id;
        protected String m_for;
        protected String m_name;
        protected String m_type;
        protected String m_dflt;
        
        protected StringBuffer m_sbuf = new StringBuffer();
        
        // node,edge,data parsing
        private String m_key;
        private int m_row = -1;
        private Table m_table = null;
        protected HashMap m_nodeMap = new HashMap();
        protected HashMap m_idMap = new HashMap();
        
        private String m_nodekey = null;
        private boolean m_directed = false;
        private boolean inSchema;
        
        public void startDocument() {
            m_nodeMap.clear();
            inSchema = true;
            
            m_edges.addColumn(SRC, int.class);
            m_edges.addColumn(TRG, int.class);
            m_edges.addColumn(SRCID, String.class);
            m_edges.addColumn(TRGID, String.class);
        }
        
        public void endDocument() throws SAXException {
            // time to actually set up the edges
            IntIterator rows = m_edges.rows();
            while (rows.hasNext()) {
                int r = rows.nextInt();

                String src = m_edges.getString(r, SRCID);
                if (!m_nodeMap.containsKey(src)) {
                    throw new SAXException(
                        "Tried to create edge with source node id=" + src
                        + " which does not exist.");
                }
                int s = ((Integer) m_nodeMap.get(src)).intValue();
                if (m_nodekey == null)
                	m_edges.setInt(r, SRC, s);

                String trg = m_edges.getString(r, TRGID);
                if (!m_nodeMap.containsKey(trg)) {
                    throw new SAXException(
                        "Tried to create edge with target node id=" + trg
                        + " which does not exist.");
                }
                int t = ((Integer) m_nodeMap.get(trg)).intValue();
                if (m_nodekey == null)
                	m_edges.setInt(r, TRG, t);
            }
            m_edges.removeColumn(SRCID);
            m_edges.removeColumn(TRGID);

            // now create the graph
            if (m_nodekey == null) {
            	m_graph = new Graph(m_nodes, m_edges, m_directed);
            }
            else {
            	m_graph = new Graph(
            			m_nodes, m_edges, m_directed, m_nodekey, SRC, TRG);
            }
            if (m_graphid != null)
                m_graph.putClientProperty(ID, m_graphid);
        }
        
        public void startElement(String namespaceURI, String localName, 
                                 String qName, Attributes atts)
        {
            // first clear the character buffer
            m_sbuf.delete(0, m_sbuf.length());
            
            if ( qName.equals(GRAPH) )
            {
                // parse directedness default
                String edef = atts.getValue(EDGEDEF);
                m_directed = DIRECTED.equalsIgnoreCase(edef);
                m_nodekey = atts.getValue(NODEKEY);
                m_graphid = atts.getValue(ID);
            }
            else if ( qName.equals(KEY) )
            {
                if ( !inSchema ) {
                    error("\""+KEY+"\" elements can not"
                        + " occur after the first node or edge declaration.");
                }
                m_for = atts.getValue(FOR);
                m_id = atts.getValue(ID);
                m_name = atts.getValue(ATTRNAME);
                m_type = atts.getValue(ATTRTYPE);
            }
            else if ( qName.equals(NODE) )
            {
                schemaCheck();
                
                m_row = m_nodes.addRow();
                
                String id = atts.getValue(ID);
                m_nodeMap.put(id, new Integer(m_row));
                m_table = m_nodes;
            }
            else if ( qName.equals(EDGE) )
            {
                schemaCheck();
                
                m_row = m_edges.addRow();
                
                // do not use the id value
//                String id = atts.getValue(ID);
//                if ( id != null ) {
//                    if ( !m_edges.canGetString(ID) )
//                        m_edges.addColumn(ID, String.class);
//                    m_edges.setString(m_row, ID, id);
//                }
                m_edges.setString(m_row, SRCID, atts.getValue(SRC));
                m_edges.setString(m_row, TRGID, atts.getValue(TRG));
                
                // currently only global directedness is used
                // ignore directed edge value for now
//                String dir = atts.getValue(DIRECTED);
//                boolean d = m_directed;
//                if ( dir != null ) {
//                    d = dir.equalsIgnoreCase("false");
//                }
//                m_edges.setBoolean(m_row, DIRECTED, d);
                m_table = m_edges;
            }
            else if ( qName.equals(DATA) )
            {
                m_key = atts.getValue(KEY);
            }
        }

        public void endElement(String namespaceURI, 
                String localName, String qName)
        {
            if ( qName.equals(DEFAULT) ) {
                // value is in the buffer
                m_dflt = m_sbuf.toString();
            }
            else if ( qName.equals(KEY) ) {
                // time to add to the proper schema(s)
                addToSchema();
            }
            else if ( qName.equals(DATA) ) {
                // value is in the buffer
                String value = m_sbuf.toString();
                String name = (String)m_idMap.get(m_key);
                DataParser dp = m_table.getColumn(name).getParser();
                try {
                	if (value.length() == 0) {
                		m_table.set(m_row, name, null);
					}
                	else {
                		Object val = dp.parse(value);
                		m_table.set(m_row, name, val);
                	}
                } catch ( DataParseException dpe ) {
                	//we also accept [R,G,B,A] format for an integer
                	//some old file make show an integer like this
                	//but we should avoid this
                	if (m_table.getColumnType(name) == int.class) {
						try {
							Object val = new ColorParser().parse(value);
	                        m_table.set(m_row, name, val);
						} catch (DataParseException e) {
							error(dpe);
						}
					}
                	else {
                		error(dpe);
                	}
                }
            }
            else if ( qName.equals(NODE) || qName.equals(EDGE) ) {
                m_row = -1;
                m_table = null;
            }
        }
        
        public void characters(char[] ch, int start, int len) throws SAXException {
            m_sbuf.append(ch, start, len);
        }

        // --------------------------------------------------------------------
        
        protected void schemaCheck() {
            if ( inSchema ) {
                inSchema = false;
            }
        }
        protected void addToSchema() {
            if ( m_name == null || m_name.length() == 0 )
                error("Empty "+KEY+" name.");
            if ( m_type == null || m_type.length() == 0 )
                error("Empty "+KEY+" type.");
            
            try {
            	DataParser dp = lookupParser(m_type);
            	if (dp == null) {
					error("No parser for "+m_type);
				}
            	if (m_dflt != null && m_dflt.length() == 0) {
            		Debug.prob("Zero length dft value for "+m_name+
            				", use null instead");
            		m_dflt = null;
				}
                Object dflt = m_dflt==null ? null : dp.parse(m_dflt);
                if ( m_for == null || m_for.equals(ALL) ) {
                	m_nodes.addColumn(m_name, dp.getType(), dflt);
                	m_nodes.getColumn(m_name).setParser(dp);
                	m_edges.addColumn(m_name, dp.getType(), dflt);
                	m_edges.getColumn(m_name).setParser(dp);
                } else if ( m_for.equals(NODE) ) {
                	m_nodes.addColumn(m_name, dp.getType(), dflt);
                	m_nodes.getColumn(m_name).setParser(dp);
                } else if ( m_for.equals(EDGE) ) {
                	if (m_name.equals(SRC)) {
                		m_edges.removeColumn(SRC);
                		m_edges.addColumn(m_name, dp.getType(), dflt);
                		m_edges.getColumn(m_name).setParser(dp);
					}
                	else if (m_name.equals(TRG)) {
                		m_edges.removeColumn(TRG);
                		m_edges.addColumn(m_name, dp.getType(), dflt);
                		m_edges.getColumn(m_name).setParser(dp);
                	}
                	else {
                		m_edges.addColumn(m_name, dp.getType(), dflt);
                		m_edges.getColumn(m_name).setParser(dp);
                	}
                } else {
                    error("Unrecognized \""+FOR+"\" value: "+ m_for);
                }
                m_idMap.put(m_id, m_name);
                
                m_dflt = null;
            } catch ( DataParseException dpe ) {
                error(dpe);
            }
        }
        protected DataParser lookupParser(String type){
        	Class cla = lookupType(type);
        	ParserFactory dftFactory = ParserFactory.getDefaultFactory();
        	if (cla != null) {// not a composite type, such as XXX:YYY
        		DataParser parser = dftFactory.getParser(cla);
        		if (parser != null) {
        			//it is a predefined type or itself is a dataparser
					return parser;
				}
        		else if (Collection.class.isAssignableFrom(cla)) {
    				//so all the data in this column should all be empty,
    				//otherwise we cannot parse it
    				Debug.prob("Only has Collection type in field");
    				return new CollectionParser(cla);
				}
        		else{
        			error("Not implement DataParser or not a Collection");
        		}
        	}
        	//then it has to be a composite type, such as XXX:YYY
        	String[] tokens = type.split(":");
        	//the most right one should be pre-defined
        	Class c = lookupType(tokens[tokens.length-1]);
        	DataParser elemParser = dftFactory.getParser(c);
        	for(int i = tokens.length-2; i>=0; i--){
        		try {
        			c = Class.forName(tokens[i]);
        		} catch (ClassNotFoundException e) {
        			error("Cannot find " + type);
        		}
        		if (!Collection.class.isAssignableFrom(c)) {
        			error("Not a collection" + c);
        		}
        		CollectionParser containerParser = new CollectionParser(c);
        		containerParser.setElemParser(elemParser);
        		elemParser = containerParser;
        	}
        	return elemParser;
        }
        protected Class lookupType(String type) {
        	String name = type;
            type = type.toLowerCase();
            if ( type.equals(INT) || type.equals(INTEGER) ) {
                return int.class;
            } else if ( type.equals(LONG) ) {
                return long.class;
            } else if ( type.equals(FLOAT) ) {
                return float.class;
            } else if ( type.equals(DOUBLE) || type.equals(REAL)) {
                return double.class;
            } else if ( type.equals(BOOLEAN) ) {
                return boolean.class;
            } else if ( type.equals(STRING) ) {
                return String.class;
            } else if ( type.equals(INT_ARRAY) ) {
                return int[].class;
            } else if ( type.equals(FLOAT_ARRAY) ) {
                return float[].class;
            } else if ( type.equals(DOUBLE_ARRAY) ) {
                return double[].class;
            } else if ( type.equals(BOOLEAN_ARRAY)) {
            	return boolean[].class;
            } else if ( type.equals(LONG_ARRAY)) {
            	return long[].class;
            } else if ( type.equals(STRING_ARRAY)){
            	return String[].class;
            } else if (!type.contains(":")){
            	try {
					return Class.forName(name);
				} catch (ClassNotFoundException e) {
					throw new IllegalArgumentException("Cannot find: "+name);
				}
            } else {
            	return null;
            }
        }        
        public Graph getGraph() {
            return m_graph;
        }
        protected void error(String s) {
            throw new RuntimeException(s);
        }
        
        protected void error(Exception e) {
            throw new RuntimeException(e);
        }
        
    } // end of inner class GraphMLHandler

} // end of class XMLGraphReader
